/**
 *  @file MessageDigest.cpp
 */
#include "openssl/ssl.h"
#include "openssl/evp.h"
#include "MessageDigest.h"
#include "../__internal/InitOpenSSL.h"
#include "../../exceptions/ArgumentException.h"
#include "../../exceptions/OutOfBoundsException.h"
#include "../../common/UtilFuncT.h"

using namespace cppflib::exceptions;
using namespace cppflib::collections;

namespace cppflib
{

namespace security
{

namespace crypto
{

PRIVATE struct _hashalgo_st {
           MessageDigest::hashalgo_t algo;
           const char * algoName;
        } hashAlgoTbl[] = {
                             {MessageDigest::HA_MD5,    "md5"},
                             {MessageDigest::HA_SHA1,   "sha1"},
                             {MessageDigest::HA_SHA256, "sha256"},
                             {MessageDigest::HA_SHA384, "sha384"},
                             {MessageDigest::HA_SHA512, "sha512"},
                             {MessageDigest::HA_NONE,   NULL}
                          };

PRIVATE const char * __GetNameByHashAlgo(MessageDigest::hashalgo_t algo)
{
   struct _hashalgo_st *pHashAlgoTbl = hashAlgoTbl;
   while (pHashAlgoTbl->algoName) {
      if (pHashAlgoTbl->algo == algo)
         return pHashAlgoTbl->algoName;
      ++pHashAlgoTbl;
   }

   return NULL;
}

PRIVATE EVP_MD_CTX * __GetMdCtxt(ptrany_t ctxt)
{
   EVP_MD_CTX *pCtxt = static_cast<EVP_MD_CTX*>(ctxt);
   return pCtxt;
}

/**
 *  Check whether context is initialized or not (since EVP_DigestFinal will clear the context)
 */
PRIVATE void __CheckMdCtxtInitialized(ptrany_t ctxt)
{
   if (!__GetMdCtxt(ctxt)->digest)
      throw ArgumentException(_S("Call 'Reinitialize' to init the context before use"));
}

MessageDigest::MessageDigest(void)
{
   cppflib::security::__internal::InitOpenSSL();

   this->hashAlgo = MessageDigest::HA_NONE;
   this->mdCtxt = NULL;
}

MessageDigest::MessageDigest(MessageDigest::hashalgo_t algo)
{
   cppflib::security::__internal::InitOpenSSL();

   this->hashAlgo = algo;
   this->mdCtxt = NULL;

   const char * pHashAlgoName = __GetNameByHashAlgo(algo);
   if (!pHashAlgoName)
      THROW_CTOR_EXCEP(ArgumentException, _S("Hash algorithm not found"));

   // init message digest context
   this->mdCtxt = ::MRP_Calloc(1, sizeof(EVP_MD_CTX));
   Reinitialize();
}

MessageDigest::~MessageDigest(void)
{
   ReleaseBeforeThrowing();
}

void MessageDigest::ReleaseBeforeThrowing()
{
   if (mdCtxt) {
      EVP_MD_CTX_cleanup(__GetMdCtxt(mdCtxt)); // in case it is not cleaned yet (prevent memory leak)
      cppmempool::Release(mdCtxt);
      mdCtxt = NULL;
   }

   cppflib::security::__internal::UninitOpenSSL();
}

CFString MessageDigest::GetHashName()
{
   __CheckMdCtxtInitialized(mdCtxt);
   return CFString(EVP_MD_name(EVP_MD_CTX_md(__GetMdCtxt(mdCtxt))));
}

MessageDigest::hashalgo_t MessageDigest::GetHashAlgorithm()
{
   return hashAlgo;
}

/**
 *  Return number of bytes of the final hash value
 */
sz_t MessageDigest::GetHashSize()
{
   __CheckMdCtxtInitialized(mdCtxt);
   return EVP_MD_CTX_size(__GetMdCtxt(mdCtxt));
}

/**
 *  Call this to reset the md context so as to ready for computing the hash value of other data.
 *  
 *  @note 'ComputeHash' will call this for you but not the case for 'UpdateBlock'
 */
void MessageDigest::Reinitialize()
{
   EVP_MD_CTX_cleanup(__GetMdCtxt(mdCtxt)); // in case it is not cleaned yet (prevent memory leak)
   const EVP_MD *md = EVP_get_digestbyname(__GetNameByHashAlgo(hashAlgo));
   EVP_DigestInit(__GetMdCtxt(mdCtxt), md);
}

/**
 *  Direct compute the hash value from a block of data
 *
 *  @return the byte array of the hash value (size will be same as GetHashSize)
 */
ArrayByte * MessageDigest::ComputeHash(ArrayByte & inData)
{
   return ComputeHash(inData, 0, inData.GetSize());
}

ArrayByte * MessageDigest::ComputeHash(ArrayByte & inData, u32_t offset, u32_t count)
{
   if (offset >= inData.GetSize())
      throw OutOfBoundsException(_S("offset exceeds input data size"));

   Reinitialize(); // reinit first
   ArrayByte *pHashValue = new ArrayByte(static_cast<u32_t>(GetHashSize()));

   // use smaller one
   count = cppflib::Min<u32_t>(count, inData.GetSize() - offset);
   EVP_DigestUpdate(__GetMdCtxt(mdCtxt), inData.GetRawPtr() + offset, count);
   // Final will clear 'mdCtxt' 
   EVP_DigestFinal(__GetMdCtxt(mdCtxt), pHashValue->GetRawPtr(), NULL);
   return pHashValue;
}
    
/**
 *  This is to pass in data block by block to compute the final hash value.
 *  Call this repeatedly until the final block and call 'GetHash' to get 
 *  the computed hash value.
 */
void MessageDigest::UpdateBlock(ArrayByte & inData)
{
   UpdateBlock(inData, 0, inData.GetSize());
}

void MessageDigest::UpdateBlock(ArrayByte & inData, u32_t offset, u32_t count)
{
   if (offset >= inData.GetSize())
      throw OutOfBoundsException(_S("offset exceeds input data size"));

   __CheckMdCtxtInitialized(mdCtxt); 

   // use smaller one
   count = cppflib::Min<u32_t>(count, inData.GetSize() - offset);
   EVP_DigestUpdate(__GetMdCtxt(mdCtxt), inData.GetRawPtr() + offset, count);
}

/**
 *  Get the final hash value after a series of 'UpdateBlock' calls
 *
 *  @param [in, out] outHash -- an array of byte which should be at least the size of 'GetHashSize'
 *
 *  @return size of the hash value
 */
sz_t MessageDigest::GetHash(ArrayByte & outHash)
{
   if (outHash.GetSize() < static_cast<u32_t>(GetHashSize()))
      throw ArgumentException(_S("array isn't big enough to hold the hash value"));

   __CheckMdCtxtInitialized(mdCtxt);

   u32_t hashSize = 0;
   // Final will clear 'mdCtxt' 
   EVP_DigestFinal(__GetMdCtxt(mdCtxt), outHash.GetRawPtr(), &hashSize);
   return hashSize;
}


} // end of namespace crypto

} // end of namespace security

} // end of namespace cppflib
